#!/usr/bin/python

import types

class AnyType :
	"""
	Special type that any other non-None object will match
	in an arg_types check.
	"""
	pass

class CallableType :
	"""
	Special type that any callable will match in an arg_types check.
	
	This class is included because Python does not have a generic callable
	type (although it has a built in 'callable' method).
	"""
	pass
	
def arg_types (*types, **kwargs):
	"""
	Decorator to check that a arguments given to a function is of the specified
	types. You can also instruct it to allow None arguments by setting the 
	'nullable' keyword argument to True.
	
	A TypeError will be thrown if the number of declared arguments does not
	match the implemented function, or if the type of an argument given to the
	function does not match the declaration.
	
	Do not use this decorator in speed critical code blocks.
	
	Example:
	
	 	@arg_types(MyClass, basestring, int)
		def my_method (self, name, count):
			...
	
	"""
	nullable = False
	if kwargs.has_key("nullable"):
		nullable = bool(kwargs["nullable"])
	
	# Check that we have same number of declared arguments as impl
	def dispatch (func):
		if not (len(types) == func.func_code.co_argcount):
			raise TypeError ("Function %s declared to have %s arguments, but implementation has %s"
					 		 % (func.func_name, len(types), func.func_code.co_argcount))
					 		 
		# Call the method and check argument types
		def wrapper (*func_args):
			for i in range(len(types)):
				# Allow None arguments if instructed to
				if nullable:
					if func_args[i] is None : continue
				
				# Check special types. If one of these fails the exception
				# will be thrown from the below isinstance check.
				if types[i] == AnyType:
					if not func_args[i] is None: continue
				elif types[i] == CallableType:
					if callable (func_args[i]): continue 
					
				if not isinstance(func_args[i], types[i]):
					raise TypeError ("Function %s declared to use a %s as argument %s, but got a %s"
									 % (func.func_name, types[i], i, type(func_args[i])))
			return func(*func_args)
		
		return wrapper
		
	return dispatch
	

class UserError (Exception):
	"""
	A UserError is thrown if there is an error that is entirely on the
	user side.
	
	All Exceptions that are not subclasses of UserError are considered
	internal system errors and will likely bring the compilation to a halt.
	
	The message contained in the error should be targetted towards the user,
	and contain a precise statement of the problem with as much debugging 
	help as we can provide.
	"""
	@arg_types (Exception, basestring)
	def __init__ (self, message):
		Exception.__init__ (self, message)

class InvalidArgumentError (UserError):
	"""
	Thrown if the user tries to pass an invalid argument to a tool.
	"""
	def __init__ (self, message):
		UserError.__init__ (self, message)

class ToolArgumentSpec:
	"""
	Abstraction used to add checked arguments to Tool implementations.
	The actual arguments passed to a tool are instances of ToolArguments.
	"""
	
	REQUIRED = 0
	OPTIONAL = 1
	DEPRECATED = 2
	
	LAST_REQUIREMENT = 3
	
	@arg_types (object, basestring, CallableType, int, basestring)
	def __init__ (self, name, checker, requirement, description):
		"""
		@param name: Unique name used to identify the tool argument
		@param checker: A callable used to validate the value of this argument. 
		    A checker should throw an InvalidArgumentError on invalid argument values.
		@param requirement: One of L{ToolArgumentSpec.REQUIRED}, L{ToolArgumentSpec.OPTIONAL}, or L{ToolArgumentSpec.DEPRECATED}
		@param description: A short description of the argument
		"""
		if not (requirement < self.LAST_REQUIREMENT and requirement >= 0):
			raise TypeError ("'requirement' argument is out of range, "
							 + "found %s, should be between 0 and %s"
							 % (requirement, self.LAST_REQUIREMENT))
		
		self._name = name
		self._checker = checker
		self._requirement = requirement
		self._description = description
		
	def get_name (self):
		return self._name
	
	def get_checker (self):
		return self._checker
	
	def check (self, value):
		"""
		Convenience method to run a check on a value.
		The checker will throw an InvalidArgumentError if the
		check fails.
		"""
		self._checker (value)
	
	def get_description (self):
		return self._description

def checkInteger (value):
	"""
	Simple checker for integer values for use in ToolArgumentSpecs.
	"""
	try:
		int (value)
	except ValueError:
		raise InvalidArgumentError ("'%s' is not a valid integer")

def checkBoolean (value):
	"""
	Simple checker for boolean values for use in ToolArgumentSpecs.
	"""
	try:
		int (value)
	except ValueError:
		raise InvalidArgumentError ("'%s' is not a valid boolean")

class ToolArgument :
	"""
	An argument passed to a Tool in tool.invoke. A ToolArgument argument
	instance points to a ToolArgumentSpec to which it complies.
	"""
	@arg_types (object, ToolArgumentSpec, AnyType)
	def __init__ (self, arg_spec, value):
		self._arg_spec = arg_spec
		self._value = value
		
		# This throws an InvalidArgumentError if the check fails (by design)
		arg_spec.check (value)
	
	def get_value (self):
		return self._value
	
	def get_name (self):
		return self._arg_spec.get_name ()

class MalformedToolSpec (Exception):
	"""
	Raised if there are errors setting up a tool spec.
	"""
	def __init__ (self, message):
		Exception.__init__ (self, message)
			
class ToolSpec:
	"""
	
	"""
	
	_TOOLS = {}
	"""Private map to hold all registered tool specs"""
	
	@arg_types (object, basestring, list)
	def __init__ (self, name, arg_specs):
		self._arg_specs = {}
		self._name = name
		
		for spec in arg_specs:
			if not isinstance (spec, ToolArgumentSpec):
				raise TypeError ("Found non ToolArgumentSpec '%s' in argument "
								 + "spec list" % spec)
			if self._arg_specs.has_key (spec.get_name ()):
				raise MalformedToolSpec ("DToolSpec._TOOLSuplicate argument name '%s'" 
										 % spec.get_name ())

			# All ok, register argument spec
			self._arg_specs[spec.get_name()] = spec
		
		# Assert that there isn't already a tool by our name
		if self._TOOLS.has_key (name):
			raise MalformedToolSpec ("Duplicate tool name '%s'" % name)
		
		# Register tool spec
		self._TOOLS[name] = self
	
class Tool :
	"""
	Instance of a tool
	"""
	@arg_types (object, ToolSpec)
	def __init__ (self, tool_spec):
		"""
		To create a Tool instance use Tool.get(name).
		"""
		self._spec = tool_spec
	
	@staticmethod
	def get (name):
		"""
		Create an instance of a tool
		"""
		if ToolSpec._TOOLS.has_key (name):
			spec = ToolSpec._TOOLS[name]
			return Tool (spec)
		else:
			# FIXME: Check grill.tools for implementating module and import
			#        on-the-fly
			raise NotImplementedError
	
	@staticmethod
	def exists (name):
		"""
		Return True iff a tool by the given name is available
		"""
		if ToolSpec._TOOLS.has_key (name):
			return True
		else:
			# FIXME: Check grill.tools for implementating module
			raise NotImplementedError
	
	@staticmethod
	def list_all ():
		"""
		List all available tools by name
		"""
		raise NotImplementedError
